Last updated: 2025-05-28

Checks: 6 1

Knit directory: /mnt/central_nas/projects/type1_diabetes/nathan/BlockCourse/2024_BlockCourse/T1D_analysis/

This reproducible R Markdown analysis was created with workflowr (version 1.7.1). The Checks tab describes the reproducibility checks that were applied when the results were created. The Past versions tab lists the development history.


The R Markdown file has unstaged changes. To know which version of the R Markdown file created these results, you’ll want to first commit it to the Git repo. If you’re still working on the analysis, you can ignore this warning. When you’re finished, you can run wflow_publish to commit the R Markdown file and build the HTML.

Great job! The global environment was empty. Objects defined in the global environment can affect the analysis in your R Markdown file in unknown ways. For reproduciblity it’s best to always run the code in an empty environment.

The command set.seed(12345) was run prior to running the code in the R Markdown file. Setting a seed ensures that any results that rely on randomness, e.g. subsampling or permutations, are reproducible.

Great job! Recording the operating system, R version, and package versions is critical for reproducibility.

Nice! There were no cached chunks for this analysis, so you can be confident that you successfully produced the results during this run.

Great job! Using relative paths to the files within your workflowr project makes it easier to run your code on other machines.

Great! You are using Git for version control. Tracking code development and connecting the code version to the results is critical for reproducibility.

The results in this page were generated with repository version 955a62a. See the Past versions tab to see a history of the changes made to the R Markdown and HTML files.

Note that you need to be careful to ensure that all relevant files for the analysis have been committed to Git prior to generating the results (you can use wflow_publish or wflow_git_commit). workflowr only checks the R Markdown file, but you know if there are other scripts or data files that it depends on. Below is the status of the Git repository when the results were generated:


Ignored files:
    Ignored:    T1D_analysis/figure/
    Ignored:    processing/

Unstaged changes:
    Modified:   T1D_analysis/01_ImportData_cells_Compressed.html
    Modified:   T1D_analysis/02_SpilloverCompensation_cells_Compressed.html
    Modified:   T1D_analysis/03_TransformCorrect_cells_Compressed.html
    Modified:   T1D_analysis/05_CellCategories_cells_Compressed.Rmd
    Modified:   T1D_analysis/05_CellCategories_cells_Uncompressed.Rmd

Note that any generated files, e.g. HTML, png, CSS, etc., are not included in this status report because it is ok for generated content to have uncommitted changes.


These are the previous versions of the repository in which changes were made to the R Markdown (T1D_analysis/05_CellCategories_cells_Uncompressed.Rmd) and HTML (T1D_analysis/05_CellCategories_cells_Uncompressed.html) files. If you’ve configured a remote Git repository (see ?wflow_git_remote), click on the hyperlinks in the table below to view the files as they were in that past version.

File Version Author Date Message
Rmd 955a62a nathansteenbuck 2025-05-28 full update 2025 annotation
Rmd 35c060e nathansteenbuck 2025-05-27 minor updates pipeline
Rmd 3c04956 nathansteenbuck 2024-11-21 final script added
Rmd f9ec97c nathansteenbuck 2024-11-20 adjust descriptions
Rmd 43dff72 nathansteenbuck 2024-11-19 update analysis_pipeline
Rmd 1f3aba7 nathansteenbuck 2024-11-12 T1D_analysis with uncompressed cell type annotation
Rmd d572272 nathansteenbuck 2024-11-11 T1D_analysis v1

Goal

In the following scripts, cell types are attributed to all cells in the dataset in an iterative way:

Rscript -e “rmarkdown::render(‘2024_BlockCourse/T1D_analysis/05_CellCategories_cells_Uncompressed.Rmd’)”

This is performed by performing PhenoGraph clustering using the Rphenoannoy package.

For cell type annotation we perform unsupervised clustering, and then relate the lineage marker expression of clusters to cell types.

The resulting cell categories, which are used in downstream analyses are stored as colData(spe)$cell_type.

Settings

Load packages

suppressPackageStartupMessages(c(
  library(data.table),
  library(dplyr),
  library(SpatialExperiment),
  library(parallel),
  library(tictoc),
  library(purrr),
  library(furrr)
))

Paths and settings

# Paths
if (!dir.exists(paths$folder_script)) dir.create(paths$folder_script)
plotsave_param$path <- paths$folder_script
plotsave_param_large$path <- paths$folder_script

# Misc settings
today <- gsub("-", "", Sys.Date())

Read in the data

Load the SpatialExperiment (SPE) object saved at the previous step.

fn_spe <- file.path(paths$folder_out, paste0(paths$object_type, "_", paths$panel_type, ".rds"))
spe <- readRDS(fn_spe)
print(spe)
class: SpatialExperiment 
dim: 31 83537 
metadata(1): spillover_matrix
assays(6): counts compcounts ... scaled fastMNN_case_id
rownames(31): H3 SST ... DNA1 PPY
rowData names(13): channel metal ... ...12 channel_name
colnames(83537): 6238_Uncompressed_001_1 6238_Uncompressed_001_2 ...
  6396_Uncompressed_030_775 6396_Uncompressed_030_776
colData names(30): case_id panel ... HbA1c C_peptide
reducedDimNames(0):
mainExpName: NULL
altExpNames(0):
spatialCoords names(2) : cell_x cell_y
imgData names(1): sample_id
imgloader <- function(x, image_dir, image_names,
                     suffix_rem = "", suffix_add = "",
                     bit_depth = 16, type, ...) {
  require(cytomapper)

  image_list <- file.path(image_dir, image_names)

  # Test if the image list exist
  test_exist <- which(!file.exists(image_list))
  if (length(test_exist) > 0) {
    stop(c("The following images were not found:\n",
           paste(image_list[test_exist], collapse = "\n")))
  } else {
    # Load and scale the images
    images <- loadImages(image_list, ...)
    # images <- scaleImages(images, (2 ^ bit.depth) - 1)

    # Add image names to metadata
    mcols(images)$ImageName <- gsub(suffix_rem, "", names(images))
    mcols(images)$ImageName <- paste0(mcols(images)$ImageName, suffix_add)

    # Add channel names
    if (type == "stacks") {
      print("Loading image stacks")
      channelNames(images) <- rownames(x)
    }

    return(images)
  }
}

Clustering

Settings

Load packages

RPhenoannoy can installed with: devtools::install_github("stuchly/Rphenoannoy@8b81e2e7fb0599f45070e2cba1b28ac219b7c472")

suppressPackageStartupMessages(c(
  library(ggplot2),
  library(scater),
  library(scuttle),
  library(scran),
  library(igraph),
  library(Rphenoannoy),
  library(clustree),
  library(ranger),
  library(patchwork),
  library(BiocParallel),
  library(tictoc)
))

Channels and assays

Select channels (channels_clust), assays (assay_sel) and clustering methods (methods_sel) to use.

methods_sel <- c("Pheno1")

assay_sel <- c("scaled")
names(assay_sel) <- c("scaled")

writeLines(c("Assays:", assay_sel[assay_sel %in% assayNames(spe)], 
                        assay_sel[assay_sel %in% reducedDimNames(spe)]))
Assays:
scaled
dimred_sel <- c("UMAP")
writeLines(c("\nReduced dimensions:", dimred_sel))

Reduced dimensions:
UMAP
channels <- rownames(spe)[!(grepl("DNA|H3", rownames(spe)))]
cat(c("\nChannels:", channels[channels %in% rownames(spe)]))

Channels: SST INS CD44 GLUT1 CD99 CD68 MPO SMA CD20 AMY CD3e CK19 ProINS GCG PDX1 SYP CD45RO FOXP3 CD45RA CD8a CA9 IAPP NKX6_1 CD4 CD31 Ecdh PTPRN PCSK2 PPY
cat(c("\nNumber of channels:", length(channels)))

Number of channels: 29

Select channels to use for clustering

Reduced dimension plots showing marker expression that generated by the previous script can be used to select the most relevant markers for clustering.

channels_clust <- rownames(rowData(spe)[rowData(spe)$clustering == 1, ])
cat(c("\nChannels used for unsupervised clustering:",
      channels_clust[channels_clust %in% rownames(spe)]))

Channels used for unsupervised clustering: SST INS CD44 GLUT1 CD99 CD68 MPO SMA CD20 AMY CD3e CK19 ProINS GCG PDX1 SYP CD45RO FOXP3 CD45RA CD8a CA9 IAPP NKX6_1 CD4 CD31 Ecdh PTPRN PCSK2 PPY

Subset

# Extract the metadata (to avoid conflicts when merging the SCEs)
meta <- metadata(spe)
spatial_coords <- spatialCoords(spe)
colpairs <- colPairs(spe)

# Convert to SingleCellExperiment
sce <- as(spe, "SingleCellExperiment")
remove(spe)

Clustering: PhenoGraph

Unsupervised clustering is performed with the PhenoGraph algorithm. This method works by generating a nearest-neighbor (kNN) graph of phenotpyic similarities followed by Louvain community dectection.

Here, we try the Rphenoannoy implementation. RPhenoannoy implements a parallel Jaccard-coefficient, approximates the kNN and uses Louvain clustering.

clust_method <- c("Pheno1")

# Number of nearest-neighbors
k <- 30
cur_assay <- "scaled"

# Run Phenograph - PhenoGraph for exprs and scaled assay.

clust_name <- paste(clust_method, cur_assay, sep = "_")
writeLines(c("\n", clust_name))


Pheno1_scaled
if (!clust_name %in% colnames(colData(sce))) {
  set.seed(seed)
  # Run Rphenograph.
  cur_pheno_annoy <- Rphenoannoy::Rphenoannoy(t(assay(sce, cur_assay))[, channels_clust], k = k)

  cur_pheno <- DataFrame(cur_pheno_annoy[[2]]$membership)
  colnames(cur_pheno) <- clust_name
  rownames(cur_pheno) <- colnames(assay(sce, cur_assay))

  # Add Phenograph clusters to the colData of the SCE object
  # Cluster `0` is attributed to non-subsetted cells and not islet cells
  colData(sce)[, clust_name] <- cur_pheno
  remove(cur_pheno)
  }
Run Rphenograph starts:
  -Input data of 83537 rows and 29 columns
  -k is set to 30
  Finding nearest neighbors...DONE ~ 20.918 s
  Compute jaccard coefficient between nearest-neighbor sets...
Presorting knn...
presorting DONE ~ 3.946 s
  Start jaccard
DONE ~ 0.109 s
  Build undirected graph from the weighted links...DONE ~ 2.42 s
  Run louvain clustering on the graph ...DONE ~ 30.638 s
Run Rphenograph DONE, totally takes 54.085s.
  Return a community class
  -Modularity value: 0.8468253 
  -Number of clusters: 26

Visualize clusters

Load packages

suppressPackageStartupMessages(c(
  library(heatmaply),
  library(htmltools)#,
  #library(cytomapper)
))

Visualize clusters on reduced dimensions

RUN UMAP again.

set.seed(222)
if (!("subset" %in% names(metadata(sce)))) {
  # Cells per case
  nb_cells <- 7500
  
  # Subset the SPE object (nb_cells per case)
  cell_subset <- tibble(rn = rownames(colData(sce)),
                        case_id = colData(sce)$case_id) |> 
    group_by(case_id) |>
    sample_n(nb_cells) |> 
    pull(rn)
  
  # Keep the subset cell ids in SPE metadata
  metadata(sce)[["subset"]] <- sort(as.vector(cell_subset))
}

# Keep only the selected cells and channels
spe_sub <- sce[channels_clust, metadata(sce)[["subset"]]]
# Add UMAPs to SPE object
cur_assay <- "scaled"
dimred_name <- paste("UMAP", cur_assay, sep = "_")
print(dimred_name)
[1] "UMAP_scaled"
# Run UMAP on a cell subset
if ((!dimred_name %in% reducedDimNames(spe_sub)) && ("UMAP" %in% dimred_sel)) {
  # Extract Counts. 
  if (cur_assay %in% assayNames(spe_sub)) {
    counts <- t(assay(spe_sub, cur_assay))
  }
  # Run UMAP.
  umap_model <- uwot::umap(counts, ret_model = TRUE)
  # Extract Embedding.
  cur_umap <- umap_model$embedding
  colnames(cur_umap) <- c("UMAP1", "UMAP2")
  rownames(cur_umap) <- rownames(counts)
}

reducedDim(spe_sub, dimred_name) <- cur_umap
cur_method <- "Pheno1"
cur_dimred <- "UMAP"
cur_dat <- makePerCellDF(spe_sub, use_dimred = TRUE) |>
  dplyr::arrange(case_id, donor_type)  |> 
  tibble::as_tibble()

dimred_name <- paste(cur_dimred, cur_assay, sep = "_")
clust_name <- paste(cur_method, cur_assay, sep = "_")

# Plot all clustersdat, dimred, color_by
p <- plot_dim_red(dat = cur_dat, dimred = dimred_name, color_by = clust_name,
                  sample = TRUE, size = 0.1, alpha = 1)
print(p)

fn <- paste0(paste(today, "Clusters", clust_name, cur_dimred,
                    sep = "_"), ".png")
do.call(ggsave, c(list(fn, p), plotsave_param))

Heatmap of marker expression by cluster

name_cur_assay <- "scaled"
clust_name <- paste(cur_method, cur_assay, sep = "_")
message(clust_name)
Pheno1_scaled
channels_clust2 <- channels_clust[!channels_clust %in% c("Rb", "FOXP3", "PTPRN", "CD20")]
# Summarize the data
hm <- summarize_heatmap(sce,
                        expr_values = name_cur_assay,
                        cluster_by = clust_name,
                        channels = channels_clust2)

# Display the heatmap
fn <- paste0(paste(today, "Clusters", clust_name, "Heatmap",
                    sep = "_"), ".html")
heatmaply(
    heatmaply::normalize(hm), main = clust_name, 
    file = file.path(paths$folder_script, fn))
# remove all clusters with less than 10 cells and plot again.
clust_freq <- table(colData(sce)[[clust_name]])
clust_freq

   1    2    3    4    5    6    7    8    9   10   11   12   13   14   15   16 
7906 7420 2527 5728 8678 7237 2997 3035 6104 2416  945  130 2558 3271 4309 5161 
  17   18   19   20   21   22   23   24   25   26 
4397 2808  817  276  359  192 1665  287 1311 1003 

Rare Clusters: 12, 24, 21, 22 (all below 200).

Visualize clusters with cytomapper and cytoviewer

Here, we use the cytoviewer package to visualize the clusters on the images. Thereby, we can check if the clusters are biologically meaningful and if they indeed correspond to the respective cell types.

Additional information we can use is their spatial localization of cell types, i.e. which cells in the endocrine or exocrine compartment.

Select cluster(s) and assay to show

# viz_clust <- c(26, 23, 12) # Select cluster(s) to visualize.
viz_clust <- NULL
viz_method <- "Pheno1"
viz_assay <- "scaled"

Load images and masks

if (!is.null(viz_clust)) {
  nb_images <- 14
  image_extension <- ".tiff"
  clust_name <- paste(viz_method, viz_assay, sep = "_")

  # Subset the SCE
  sce_viz <- sce[, colData(sce)[[clust_name]] %in% viz_clust]

  # Select random image.
  set.seed(seed)
  image_sub <- sort(sample(
    unique(sce_viz$image_fullname),
  min(length(unique(sce_viz$image_fullname)), nb_images)))

  # Folders
  folder_images <- file.path(paths$folder_in, "img", paths$panel_type)
  folder_masks <- file.path(paths$folder_in, "masks_cells",
                            paths$panel_type, "whole-cell")

  # Load images and masks
  images <- imgloader(
    x = sce_viz,
    image_dir = folder_images,
    image_names = image_sub,
    type = "stacks"
  )

  masks <- imgloader(
    x = sce_viz,
    image_dir = folder_masks,
    image_names = image_sub,
    as.is = TRUE,
    type = "masks"
  )

  sce_viz <- sce_viz[, sce_viz$image_fullname %in% image_sub]
  sce_viz$ImageName <- gsub(image_extension, "", sce_viz$image_fullname)
}

Interactive with Cytoviewer

# Use cytoviewer with images, masks and object
library(cytoviewer)
if (!is.null(viz_clust)) {
  channels_view <- channels_clust
  sub_images <- cytomapper::getChannels(images, channels_view)
  app <- cytoviewer(image = sub_images, 
                    mask = masks, 
                    object = sce_viz[channels_view, ], 
                    img_id = "ImageName", 
                    cell_id = "cell_number")

  if (interactive()) {
    shiny::runApp(app)
  }
}

Attribute cell categories

Note: this section requires manual intervention

Here, clusters obtained at the previous steps are manually merged into meaningful cell types. Cluster numbers are attributed to the different cell types based on the plots and heatmaps above. Consequently, this attribution has to be adapted to the clustering results.

Cell category attribution

Cell category attribution has to be performed for every combination of clustering methods and selected assays.

Phenograph x scaled counts

Depending on the clustering, maybe select other categories.

clust_methods <- c("Pheno1")
clust_assay <- c("scaled")

if (!clust_assay %in% assay_sel) stop("The selected assay is not in the assays selected for clustering")
if (length(clust_assay) != 1) ("Select only one assay")

clust_name <- paste(clust_methods, clust_assay, sep = "_")
print(clust_name)
[1] "Pheno1_scaled"
# Islet:
clust_beta <- c(15, 14) # 
clust_alpha <- c(16, 24)
clust_delta <- c(18)
clust_gamma <- c(12)
clust_islet_other <- c(13) # 

# Immune:
clust_t <- c(11)
clust_neutrophil <- c(22)
clust_myeloid <- c(20)

# Exocrine Cells + Stromal.
clust_endothelial <- c(7)
clust_mesenchymal <- c(21)

clust_ductal <- c(9, 1, 17, 19)
clust_acinar <- c(26, 2:6, 8)
clust_other <- c(10, 23, 25)

all_clust <- sort(c(clust_alpha, clust_beta, clust_delta, clust_gamma, clust_islet_other,
                    clust_t,  clust_neutrophil, clust_myeloid,
                    clust_endothelial, clust_mesenchymal, 
                    clust_ductal, clust_acinar, clust_other))

if ((!length(unique(all_clust)) ==
     length(unique(colData(sce)[, clust_name]))) ||
    any(duplicated(all_clust))) {
  stop("Recheck cluster attribution")
}

# Add cell types to the SCE object
# Islet
colData(sce)[colData(sce)[, clust_name] %in% clust_alpha,
             "cell_type"] <- "Alpha"
colData(sce)[colData(sce)[, clust_name] %in% clust_beta,
              "cell_type"] <- "Beta"
colData(sce)[colData(sce)[, clust_name] %in% clust_delta,
              "cell_type"] <- "Delta"
colData(sce)[colData(sce)[, clust_name] %in% clust_gamma,
               "cell_type"]<- "Gamma"
colData(sce)[colData(sce)[, clust_name] %in% clust_islet_other,
              "cell_type"] <- "Islet_Other"

# Immune:
colData(sce)[colData(sce)[, clust_name] %in% clust_t,
             "cell_type"] <- "T_cell"
colData(sce)[colData(sce)[, clust_name] %in% clust_myeloid,
             "cell_type"] <- "Myeloid"
colData(sce)[colData(sce)[, clust_name] %in% clust_neutrophil,
              "cell_type"] <- "Neutrophil"

# Exocrine
colData(sce)[colData(sce)[, clust_name] %in% clust_ductal,
              "cell_type"] <- "Ductal"
colData(sce)[colData(sce)[, clust_name] %in% clust_acinar,
              "cell_type"] <- "Acinar"

# Stroma:
colData(sce)[colData(sce)[, clust_name] %in% clust_endothelial,
              "cell_type"] <- "Endothelial"
colData(sce)[colData(sce)[, clust_name] %in% clust_mesenchymal,
              "cell_type"] <- "Fibro_SM"

# Other
colData(sce)[colData(sce)[, clust_name] %in% clust_other,
              "cell_type"] <- "Other"

Assign Cell Categories base don the assigned cell types.

# Islet-category (Alpha, Beta, Delta, Gamma)
colData(sce)[colData(sce)[, "cell_type"] %in% c("Alpha", "Beta", "Delta", "Gamma", "Islet_Other"),
             "cell_category"] <- "Islet"

# Immune-category (Lympho, Myeloid, Neutrophil)
colData(sce)[colData(sce)[, "cell_type"] %in% c("T_cell", "Myeloid", "Neutrophil"),
             "cell_category"] <- "Immune"

# Exocrine-category (Ductal, Acinar)
colData(sce)[colData(sce)[, "cell_type"] %in% c("Ductal", "Acinar"),
              "cell_category"] <- "Exocrine"
# Stroma-category (Endothelial, Fibro_Endo)
colData(sce)[colData(sce)[, "cell_type"] %in% c("Endothelial", "Fibro_SM"),
              "cell_category"] <- "Stroma"

# Other-category (Other)
colData(sce)[colData(sce)[, "cell_type"] %in% c("Other"),
              "cell_category"] <- "Other"

Plot cell types on reduced dimensions

spe_sub$Pheno1_scaled <- sce[, colnames(spe_sub)]$Pheno1_scaled
spe_sub$cell_type <- sce[, colnames(spe_sub)]$cell_type

# Prepare the data
cur_dat <- makePerCellDF(spe_sub, use_dimred = TRUE) |>
  arrange(case_id, donor_type)

# Plot
cur_assay <- "scaled"
cur_dimred <- "UMAP"
cur_method <- "Pheno1"

dimred_name <- paste(cur_dimred, cur_assay, sep = "_")
clust_name <- "cell_type"

p <- plot_dim_red(cur_dat, dimred_name, clust_name,
                  sample = TRUE, size = 1, alpha = 1)
print(p)

fn <- paste0(paste(today, clust_name, cur_dimred,
                  sep = "_"), ".png")
do.call(ggsave, c(list(fn, p), plotsave_param))

Cell Types heatmap

channels_clust2 <- channels_clust[!channels_clust %in% 
                    c("Rb", "FOXP3", "CD45RA", "PTPRN", "CD20")]

cur_assay <- "scaled"
name_cur_assay <- "scaled"
clust_name <- "cell_type"

# Summarize the data
hm <- summarize_heatmap(sce,
                        expr_values = name_cur_assay,
                        cluster_by = clust_name,
                        channels = channels_clust2)

# Display the heatmap
fn <- paste0(paste(today, clust_name, "Heatmap",
                sep = "_"), ".html")

set.seed(222)
heatmaply(
heatmaply::normalize(hm), main = clust_name,
file = file.path(paths$folder_script, fn))

Number of cells by cell category

table(colData(sce)[, "cell_type"])

     Acinar       Alpha        Beta       Delta      Ductal Endothelial 
      35628        5448        7580        2808       19224        2997 
   Fibro_SM       Gamma Islet_Other     Myeloid  Neutrophil       Other 
        359         130        2558         276         192        5392 
     T_cell 
        945 
table(colData(sce)[, "cell_category"])

Exocrine   Immune    Islet    Other   Stroma 
   54852     1413    18524     5392     3356 

Plot cell categories

Reduced dimensions

Plot consensus cell categories on reduced dimensions

spe_sub$cell_category <- sce[, colnames(spe_sub)]$cell_category

ccat <- c("Immune", "Islet", "Exocrine", "Stroma", "Other")
names(ccat) <- c("Immune", "Islet", "Exocrine", "Stroma", "Other")
palette_ccat <- c(palettes$colors[1:(length(ccat) - 2)], "grey40", "grey")
names(palette_ccat) <- ccat

cur_dat <- scuttle::makePerCellDF(spe_sub,
                                  use_dimred = TRUE) |>
  arrange(case_id, donor_type)

dimred_name <- paste(cur_dimred, cur_assay, sep = "_")

p <- plot_dim_red(cur_dat, dimred_name, "cell_category",
                  sample = TRUE, size = 0.1, alpha = 1,
                  palette = palette_ccat)
print(p)

fn <- paste0(paste(today, "CellCat", "Consensus", dimred_name,
                    sep = "_"), ".png")
do.call(ggsave, c(list(fn, p), plotsave_param))

Heatmap

clust_name <- paste("cell_category", cur_assay, sep = "_")

# Summarize the data
hm <- summarize_heatmap(sce,
                        expr_values = cur_assay,
                        cluster_by = "cell_category",
                        channels = channels)

# Display the heatmap
fn <- paste0(paste(today, "CellCat", "Consensus", cur_assay,
                    "Heatmap", sep = "_"), ".html")

heatmaply(
  heatmaply::normalize(hm), main = clust_name,
  file = file.path(paths$folder_script, fn))

Save

Remove temporary columns

Save SCE

fn <- file.path(paths$folder_out, paste0(paths$object_type, "_", paths$panel_type, "sce.rds"))
saveRDS(sce, fn)

sessionInfo()
R version 4.3.1 (2023-06-16)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 20.04.6 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.9.0 
LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.9.0

locale:
 [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
 [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
 [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
[10] LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   

time zone: Etc/UTC
tzcode source: system (glibc)

attached base packages:
[1] parallel  stats4    stats     graphics  grDevices utils     datasets 
[8] methods   base     

other attached packages:
 [1] cytoviewer_1.2.0            htmltools_0.5.8.1          
 [3] heatmaply_1.5.0             viridis_0.6.5              
 [5] viridisLite_0.4.2           plotly_4.10.4              
 [7] BiocParallel_1.36.0         patchwork_1.2.0            
 [9] ranger_0.16.0               clustree_0.5.1             
[11] ggraph_2.2.1                Rphenoannoy_0.1.0          
[13] Matrix_1.6-5                igraph_2.1.4               
[15] scran_1.30.2                scater_1.30.1              
[17] scuttle_1.12.0              furrr_0.3.1                
[19] future_1.49.0               purrr_1.0.2                
[21] tictoc_1.2.1                data.table_1.17.2          
[23] SpatialExperiment_1.12.0    SingleCellExperiment_1.24.0
[25] SummarizedExperiment_1.32.0 Biobase_2.62.0             
[27] GenomicRanges_1.54.1        GenomeInfoDb_1.38.8        
[29] IRanges_2.36.0              S4Vectors_0.40.2           
[31] BiocGenerics_0.48.1         MatrixGenerics_1.14.0      
[33] matrixStats_1.5.0           dplyr_1.1.4                
[35] ggplot2_3.5.1              

loaded via a namespace (and not attached):
  [1] RcppAnnoy_0.0.22          later_1.3.2              
  [3] bitops_1.0-9              svgPanZoom_0.3.4         
  [5] tibble_3.2.1              polyclip_1.10-7          
  [7] lifecycle_1.0.4           edgeR_4.0.16             
  [9] rprojroot_2.0.4           globals_0.18.0           
 [11] lattice_0.22-7            MASS_7.3-60              
 [13] crosstalk_1.2.1           dendextend_1.17.1        
 [15] magrittr_2.0.3            limma_3.58.1             
 [17] sass_0.4.9                rmarkdown_2.27           
 [19] jquerylib_0.1.4           yaml_2.3.10              
 [21] metapod_1.10.1            httpuv_1.6.15            
 [23] sp_2.1-4                  RColorBrewer_1.1-3       
 [25] abind_1.4-8               zlibbioc_1.48.2          
 [27] RCurl_1.98-1.14           tweenr_2.0.3             
 [29] git2r_0.33.0              seriation_1.5.5          
 [31] GenomeInfoDbData_1.2.11   ggrepel_0.9.5            
 [33] irlba_2.3.5.1             listenv_0.9.1            
 [35] terra_1.7-78              dqrng_0.4.1              
 [37] parallelly_1.44.0         svglite_2.1.3            
 [39] DelayedMatrixStats_1.24.0 codetools_0.2-20         
 [41] DelayedArray_0.28.0       ggforce_0.4.2            
 [43] tidyselect_1.2.1          raster_3.6-26            
 [45] farver_2.1.2              ScaledMatrix_1.10.0      
 [47] TSP_1.2-4                 webshot_0.5.5            
 [49] jsonlite_2.0.0            BiocNeighbors_1.20.2     
 [51] tidygraph_1.3.1           iterators_1.0.14         
 [53] systemfonts_1.1.0         foreach_1.5.2            
 [55] tools_4.3.1               ragg_1.3.2               
 [57] Rcpp_1.0.14               glue_1.8.0               
 [59] gridExtra_2.3             SparseArray_1.2.4        
 [61] xfun_0.52                 EBImage_4.44.0           
 [63] HDF5Array_1.30.1          ca_0.71.1                
 [65] shinydashboard_0.7.2      withr_3.0.2              
 [67] fastmap_1.2.0             rhdf5filters_1.14.1      
 [69] bluster_1.12.0            fansi_1.0.6              
 [71] digest_0.6.37             rsvd_1.0.5               
 [73] R6_2.6.1                  mime_0.13                
 [75] textshaping_0.4.0         colorspace_2.1-1         
 [77] jpeg_0.1-11               utf8_1.2.5               
 [79] tidyr_1.3.1               generics_0.1.4           
 [81] graphlayouts_1.1.1        httr_1.4.7               
 [83] htmlwidgets_1.6.4         S4Arrays_1.2.1           
 [85] whisker_0.4.1             uwot_0.2.2               
 [87] pkgconfig_2.0.3           gtable_0.3.6             
 [89] registry_0.5-1            workflowr_1.7.1          
 [91] XVector_0.42.0            fftwtools_0.9-11         
 [93] scales_1.3.0              png_0.1-8                
 [95] knitr_1.47                reshape2_1.4.4           
 [97] rjson_0.2.23              rhdf5_2.46.1             
 [99] cachem_1.1.0              stringr_1.5.1            
[101] shinycssloaders_1.0.0     miniUI_0.1.1.1           
[103] vipor_0.4.7               pillar_1.9.0             
[105] grid_4.3.1                vctrs_0.6.5              
[107] RANN_2.6.2                promises_1.3.0           
[109] BiocSingular_1.18.0       beachmat_2.18.1          
[111] xtable_1.8-4              cluster_2.1.8.1          
[113] archive_1.1.12            beeswarm_0.4.0           
[115] evaluate_1.0.3            magick_2.8.3             
[117] cli_3.6.5                 locfit_1.5-9.9           
[119] compiler_4.3.1            rlang_1.1.6              
[121] crayon_1.5.3              labeling_0.4.3           
[123] plyr_1.8.9                fs_1.6.6                 
[125] ggbeeswarm_0.7.2          stringi_1.8.7            
[127] nnls_1.6                  cytomapper_1.14.0        
[129] assertthat_0.2.1          munsell_0.5.1            
[131] tiff_0.1-12               lazyeval_0.2.2           
[133] colourpicker_1.3.0        sparseMatrixStats_1.14.0 
[135] Rhdf5lib_1.24.2           statmod_1.5.0            
[137] shiny_1.8.1.1             highr_0.11               
[139] memoise_2.0.1             bslib_0.7.0